iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
0
Modern Web

Angular 全集中筆記系列 第 25

第 25 型 - 響應式表單 (Reactive Form) - 表單驗證

  • 分享至 

  • xImage
  •  

如同範本驅動表單 (Template-Dirven Form),也可以針對響應式表單 (Reactive Form) 進行表單驗證,這一篇就針對待辦事項加入以下的驗證。

  • 主旨與等級欄位為必填欄位
  • 待辦事項至少需要一個標籤
  • 主旨不存在於清單資料內

實作主旨與等級欄位為必填欄位

在響應式表單 (Reactive Form) 中,無論表單控制項建構式或是 FormBuilder 服務元件的方法,第二與第三的參數皆用來定義一表單驗證陣列,前者為同步型的表單驗證,後者則為非同步。

因此,在 task-form.component.ts 定義表單時,針對主旨與等級加入 required 驗證,並加入主旨與等於的 get 屬性。

export class TaskFormComponent implements OnInit {
  get subject(): FormControl {
    return this.form.get('subject') as FormControl;
  }

  get level(): FormControl {
    return this.form.get('level') as FormControl;
  }

  ngOnInit(): void {
    this.form = this.fb.group({
      subject: this.fb.control(undefined, [Validators.required]),
      state: this.fb.control(0),
      level: this.fb.control(undefined, [Validators.required]),
      tags: this.fb.array([]),
    });
  }
}

接著就可以在 task-form.component.html 利用 FormControl 的 invalidhasError 來判斷是否顯示錯誤訊息,並在 task-form.component.css 中加入錯誤訊息的樣式。

<form [formGroup]="form">
  <div class="form-field">
    <label>主旨</label>
    <input type="text" formControlName="subject" />
  </div>
  <ul *ngIf="subject.touched && subject.invalid">
    <li *ngIf="subject.hasError('required')">需要輸入主旨</li>
  </ul>
  <div class="form-field">
    <label>等級</label>
    <select formControlName="level">
      <option value=""></option>
      <option value="XS">XS</option>
      <option value="S">S</option>
      <option value="M">M</option>
      <option value="L">L</option>
      <option value="XL">XL</option>
    </select>
  </div>
  <ul *ngIf="level.touched && level.invalid">
    <li *ngIf="level.hasError('required')">需要輸入等級</li>
  </ul>
</form>
ul {
  margin: 0;
  margin-top: -12px;
  padding-left: 100px;
  font-size: 10pt;
  color: red;
}

Result

自訂標籤陣列不得為空驗證

當 Angular 所內建的表單驗證無法滿足需求時,可以自訂一表單驗證方法來實作需求,此方法會回傳 ValidationErrors。因此,可以在 task-form.component.ts 加入標籤陣列是否為空的驗證方法,並設定在 tags 表單屬性內。

export class TaskFormComponent implements OnInit {
  ngOnInit(): void {
    this.form = this.fb.group({
      subject: this.fb.control(undefined, [Validators.required]),
      state: this.fb.control(0),
      level: this.fb.control(undefined, [Validators.required]),
      tags: this.fb.array([], [this.arrayCannotEmpty]),
    });
  }

  arrayCannotEmpty(control: FormArray): ValidationErrors {
	if (control.length === 0) {
	  return { cannotEmpty: true };
	}
	return null;
  }
}

ArrayCannontEmpty() 的驗證方法中,會傳入所使用表單控制項物料,此案例為表單陣列 tags,當此陣列為空時回傳一物件,此物件即為 ValidationErrors,會在驗證不通過時將此值寫入表單控制項的 errors 屬性內;因此在通過驗證時,所回傳的值需為 null

另外,在需額外指定條件的需求中,例如 'Validators.min()' 表單驗證,可以定義一回傳 ValidatorFn 的驗證方法來實作;因此上面程式可以修改成:

export class TaskFormComponent implements OnInit {
  ngOnInit(): void {
    this.form = this.fb.group({
      subject: this.fb.control(undefined, [Validators.required]),
      state: this.fb.control(0),
      level: this.fb.control(undefined, [Validators.required]),
      tags: this.fb.array([], [this.arrayCannotEmpty()]),
    });
  }

  arrayCannotEmpty(): ValidatorFn {
    return (control: FormArray) => {
      if (control.length === 0) {
        return { cannotEmpty: true };
      }
      return null;
    };
  }
}

最後,在頁面上透過是否有 cannotEmpty 錯誤的判斷,來加入標籤陣列的錯誤訊息顯示。

<form [formGroup]="form">
  <div class="tags-field">
    <button type="button" (click)="onAddTag()">標籤新增</button>
    <div formArrayName="tags" *ngFor="let control of tags.controls; let index = index">
      <input type="text" [formControl]="control" />
      <button type="button" (click)="onDeleteTag(index)">刪除</button>
    </div>
  </div>
  <ul *ngIf="tags.invalid">
    <li *ngIf="tags.hasError('cannotEmpty')">標籤不得為空</li>
  </ul>
</form>

Result

自訂非同步驗證方法檢查主旨是否有重覆

由於主旨的重覆性檢查,會依後端服務所回傳的結果來判斷,因此需要自訂非同步的驗證方法,此方法會回傳 Observable 型別物件。首先,在 task.service.ts 實作主旨是否已存在的方法。

export class TaskRemoteService {
  isExists(subject: string): Observable<boolean> {
    const url = `${this._url}?subject=${subject}`;
    return this.httpClient.get<Task[]>(url).pipe(map((tasks) => tasks.length > 0));
  }
}

接著,在 task-form.component.ts 中加入驗證方法,當值有被輸入時透過 TaskRemoteService 來判斷是否已存在,並將此方法設定至主旨驗證中。

export class TaskFormComponent implements OnInit {
  ngOnInit(): void {
    this.form = this.fb.group({
      subject: this.fb.control(undefined, [Validators.required], [this.shouldBeUnique.bind(this)]),
      state: this.fb.control(0),
      level: this.fb.control(undefined, [Validators.required]),
      tags: this.fb.array([], [this.arrayCannotEmpty]),
    });
  }

  shouldBeUnique(control: FormControl): Observable<ValidationErrors> {
    if (control.value) {
      return this.taskService.isExists(control.value).pipe(map((isExists) => (isExists ? { shouldBeUnique: true } : null)));
    } else {
      return of(null);
    }
  }
}

最後,在 task-form.component.html 中加入錯誤訊息的顯示。

<form [formGroup]="form">
  <div class="form-field">
    <label>主旨</label>
    <input type="text" formControlName="subject" />
  </div>
  <ul *ngIf="subject.touched && subject.invalid">
    <li *ngIf="subject.hasError('required')">需要輸入主旨</li>
    <li *ngIf="subject.hasError('shouldBeUnique')">主旨已存在</li>
  </ul>
</form>

Result

結論

透過響應式表單 (Reactive Form) 的開發方法,再加上 Rxjs 的使用,針對較為複雜的表單,會有很大彈性的靈活度。


上一篇
第 24 型 - 響應式表單 (Reactive Form) - 表單陣列
下一篇
第 26 型 - 路由 (Router)
系列文
Angular 全集中筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言